use std::io::fs::{mkdir_recursive,rmdir_recursive};
use serialize::{Encodable,Encoder};
use url::Url;
+use git2;
-use util::{CargoResult, ChainError, ProcessBuilder, process, human};
+use util::{CargoResult, ChainError, human, ToUrl, internal, Require};
#[deriving(PartialEq,Clone,Encodable)]
pub enum GitReference {
}
}
-macro_rules! git(
- ($config:expr, $($arg:expr),+) => (
- try!(git_inherit(&$config, process("git")$(.arg($arg))*))
- )
-)
-
-macro_rules! git_output(
- ($config:expr, $($arg:expr),*) => ({
- try!(git_output(&$config, process("git")$(.arg($arg))*))
- })
-)
-
-macro_rules! errln(
- ($($arg:tt)*) => (let _ = writeln!(::std::io::stdio::stderr(), $($arg)*))
-)
-
/// GitRemote represents a remote repository. It gets cloned into a local
/// GitDatabase.
#[deriving(PartialEq,Clone,Show)]
/// GitDatabase is a local clone of a remote repository's database. Multiple
/// GitCheckouts can be cloned from this GitDatabase.
-#[deriving(PartialEq,Clone)]
pub struct GitDatabase {
remote: GitRemote,
path: Path,
+ repo: git2::Repository,
}
#[deriving(Encodable)]
/// GitCheckout is a local checkout of a particular revision. Calling
/// `clone_into` with a reference will resolve the reference into a revision,
/// and return a CargoError if no revision for that reference was found.
-pub struct GitCheckout {
- database: GitDatabase,
+pub struct GitCheckout<'a> {
+ database: &'a GitDatabase,
location: Path,
revision: GitRevision,
+ repo: git2::Repository,
}
#[deriving(Encodable)]
pub struct EncodableGitCheckout {
- database: GitDatabase,
+ database: EncodableGitDatabase,
location: String,
revision: String,
}
-impl<E, S: Encoder<E>> Encodable<S, E> for GitCheckout {
+impl<'a, E, S: Encoder<E>> Encodable<S, E> for GitCheckout<'a> {
fn encode(&self, s: &mut S) -> Result<(), E> {
EncodableGitCheckout {
- database: self.database.clone(),
location: self.location.display().to_string(),
- revision: self.revision.to_string()
+ revision: self.revision.to_string(),
+ database: EncodableGitDatabase {
+ remote: self.database.remote.clone(),
+ path: self.database.path.display().to_string(),
+ },
}.encode(s)
}
}
pub fn rev_for<S: Str>(&self, path: &Path, reference: S)
-> CargoResult<GitRevision> {
- // We simultaneously want to transform the reference into a resolved
- // revision as well as verify that the reference itself is inside the
- // repository. Sadly for a 40-character SHA1 the call to `rev-parse`
- // will *always* return the same string with a 0 exit status, regardless
- // of whether it's present in the database.
- //
- // Later versions of git introduced a syntax for this query via
- // `$sha1^{object}`, but older versions of git do not support this. To
- // get around this limitation, we chop 40-character sha revisions to 39
- // characters to get an error'd exit status if the revision is indeed
- // not present.
- let mut reference = reference.as_slice();
- if reference.len() == 40 {
- reference = reference.slice_to(39);
- }
- Ok(GitRevision(git_output!(*path, "rev-parse", reference)))
+ let db = try!(self.db_at(path));
+ db.rev_for(reference)
}
pub fn checkout(&self, into: &Path) -> CargoResult<GitDatabase> {
- if into.exists() {
- try!(self.fetch_into(into));
+ let repo = if into.exists() {
+ let r = try!(git2::Repository::open(into));
+ try!(self.fetch_into(&r).chain_error(|| {
+ internal(format!("failed to fetch into {}", into.display()))
+ }));
+ r
} else {
- try!(self.clone_into(into));
- }
+ try!(self.clone_into(into).chain_error(|| {
+ internal(format!("failed to clone into: {}", into.display()))
+ }))
+ };
- Ok(GitDatabase { remote: self.clone(), path: into.clone() })
+ Ok(GitDatabase { remote: self.clone(), path: into.clone(), repo: repo })
}
- pub fn db_at(&self, db_path: &Path) -> GitDatabase {
- GitDatabase { remote: self.clone(), path: db_path.clone() }
+ pub fn db_at(&self, db_path: &Path) -> CargoResult<GitDatabase> {
+ let repo = try!(git2::Repository::open(db_path));
+ Ok(GitDatabase {
+ remote: self.clone(),
+ path: db_path.clone(),
+ repo: repo,
+ })
}
- fn fetch_into(&self, path: &Path) -> CargoResult<()> {
- Ok(git!(*path, "fetch", "--force", "--quiet", "--tags",
- self.url.to_string(), "refs/heads/*:refs/heads/*"))
+ fn fetch_into(&self, dst: &git2::Repository) -> CargoResult<()> {
+ let url = self.url.to_string();
+ let refspec = "refs/heads/*:refs/heads/*";
+ let mut remote = try!(dst.remote_create_anonymous(url.as_slice(),
+ refspec));
+ try!(remote.add_fetch("refs/tags/*:refs/tags/*"));
+ let sig = try!(git2::Signature::default(dst));
+ try!(remote.fetch(&sig, None));
+ Ok(())
}
- fn clone_into(&self, path: &Path) -> CargoResult<()> {
- let dirname = Path::new(path.dirname());
-
- try!(mkdir_recursive(path, UserDir));
-
- Ok(git!(dirname, "clone", self.url.to_string(), path, "--bare",
- "--no-hardlinks", "--quiet"))
+ fn clone_into(&self, dst: &Path) -> CargoResult<git2::Repository> {
+ let url = self.url.to_string();
+ try!(mkdir_recursive(dst, UserDir));
+ let repo = try!(git2::build::RepoBuilder::new().bare(true)
+ .hardlinks(false)
+ .clone(url.as_slice(), dst));
+ Ok(repo)
}
}
pub fn copy_to(&self, rev: GitRevision, dest: &Path)
-> CargoResult<GitCheckout> {
-
- if dest.exists() {
- match self.remote.rev_for(dest, "HEAD") {
- Ok(ref head) if rev == *head => {
- return Ok(GitCheckout::new(dest, self.clone(), rev.clone()));
+ match git2::Repository::open(dest) {
+ Ok(repo) => {
+ let is_fresh = match repo.revparse_single("HEAD") {
+ Ok(head) => head.id().to_string() == rev.to_string(),
+ _ => false,
+ };
+ if is_fresh {
+ return Ok(GitCheckout::new(dest, self, rev, repo))
}
- _ => {}
}
+ _ => {}
}
- GitCheckout::clone_into(dest, self.clone(), rev.clone())
+ GitCheckout::clone_into(dest, self, rev)
}
pub fn rev_for<S: Str>(&self, reference: S) -> CargoResult<GitRevision> {
- self.remote.rev_for(&self.path, reference)
+ let rev = try!(self.repo.revparse_single(reference.as_slice()));
+ Ok(GitRevision(rev.id().to_string()))
}
pub fn has_ref<S: Str>(&self, reference: S) -> CargoResult<()> {
- git_output!(self.path, "rev-parse", "--verify", reference.as_slice());
+ try!(self.repo.revparse_single(reference.as_slice()));
Ok(())
}
}
-impl GitCheckout {
- fn new(path: &Path, database: GitDatabase, revision: GitRevision)
- -> GitCheckout
+impl<'a> GitCheckout<'a> {
+ fn new<'a>(path: &Path, database: &'a GitDatabase, revision: GitRevision,
+ repo: git2::Repository)
+ -> GitCheckout<'a>
{
GitCheckout {
location: path.clone(),
database: database,
revision: revision,
+ repo: repo,
}
}
- fn clone_into(into: &Path, database: GitDatabase,
- revision: GitRevision)
- -> CargoResult<GitCheckout>
+ fn clone_into<'a>(into: &Path, database: &'a GitDatabase,
+ revision: GitRevision)
+ -> CargoResult<GitCheckout<'a>>
{
- let checkout = GitCheckout::new(into, database, revision);
+ let repo = try!(GitCheckout::clone_repo(database.get_path(), into));
+ let checkout = GitCheckout::new(into, database, revision, repo);
- try!(checkout.clone_repo());
+ try!(checkout.reset());
try!(checkout.update_submodules());
Ok(checkout)
}
- fn get_source(&self) -> &Path {
- self.database.get_path()
- }
-
pub fn get_rev(&self) -> &str {
self.revision.as_slice()
}
- fn clone_repo(&self) -> CargoResult<()> {
- let dirname = Path::new(self.location.dirname());
+ fn clone_repo(source: &Path, into: &Path) -> CargoResult<git2::Repository> {
+ let dirname = into.dir_path();
try!(mkdir_recursive(&dirname, UserDir).chain_error(|| {
- human(format!("Couldn't mkdir {}",
- Path::new(self.location.dirname()).display()))
+ human(format!("Couldn't mkdir {}", dirname.display()))
}));
- if self.location.exists() {
- try!(rmdir_recursive(&self.location).chain_error(|| {
- human(format!("Couldn't rmdir {}",
- Path::new(&self.location).display()))
+ if into.exists() {
+ try!(rmdir_recursive(into).chain_error(|| {
+ human(format!("Couldn't rmdir {}", into.display()))
}));
}
- git!(dirname, "clone", "--no-checkout", "--quiet",
- self.get_source(), &self.location);
- try!(self.reset());
-
- Ok(())
+ let url = try!(source.to_url().map_err(human));
+ let url = url.to_string();
+ let repo = try!(git2::Repository::clone(url.as_slice(),
+ into).chain_error(|| {
+ internal(format!("failed to clone {} into {}", source.display(),
+ into.display()))
+ }));
+ Ok(repo)
}
fn reset(&self) -> CargoResult<()> {
- Ok(git!(self.location, "reset", "-q", "--hard",
- self.revision.as_slice()))
+ info!("reset {} to {}", self.repo.path().display(),
+ self.revision.as_slice());
+ let sig = try!(git2::Signature::default(&self.repo));
+ let oid = try!(git2::Oid::from_str(self.revision.as_slice()));
+ let object = try!(git2::Object::lookup(&self.repo, oid, None));
+ try!(self.repo.reset(&object, git2::Hard, &sig, None));
+ Ok(())
}
fn update_submodules(&self) -> CargoResult<()> {
- git!(self.location, "submodule", "sync", "--quiet");
- // Sadly older versions of git don't actually respect --quiet for *all*
- // operations and still print some thing here and there.
- git_output!(self.location, "submodule", "update", "--init",
- "--recursive", "--quiet");
- Ok(())
+ let sig = try!(git2::Signature::default(&self.repo));
+ return update_submodules(&self.repo, &sig);
+
+ fn update_submodules(repo: &git2::Repository,
+ sig: &git2::Signature) -> CargoResult<()> {
+ info!("update submodules for: {}", repo.path().display());
+
+ for mut child in try!(repo.submodules()).move_iter() {
+ try!(child.init(false));
+
+ // A submodule which is listed in .gitmodules but not actually
+ // checked out will not have a head id, so we should ignore it.
+ let head = match child.head_id() {
+ Some(head) => head,
+ None => continue,
+ };
+
+ // If the submodule hasn't been checked out yet, we need to
+ // clone it. If it has been checked out and the head is the same
+ // as the submodule's head, then we can bail out and go to the
+ // next submodule.
+ let repo = match child.open() {
+ Ok(repo) => {
+ if child.head_id() == try!(repo.head()).target() {
+ continue
+ }
+ repo
+ }
+ Err(..) => {
+ let path = repo.path().dir_path().join(child.path());
+ let url = try!(child.url().require(|| {
+ internal("invalid submodule url")
+ }));
+ try!(git2::Repository::clone(url, &path))
+ }
+ };
+
+ // Fetch data from origin and reset to the head commit
+ let url = try!(child.url().require(|| {
+ internal("repo with non-utf8 url")
+ }));
+ let refspec = "refs/heads/*:refs/heads/*";
+ let mut remote = try!(repo.remote_create_anonymous(url, refspec));
+ try!(remote.fetch(sig, None));
+
+ let obj = try!(git2::Object::lookup(&repo, head, None));
+ try!(repo.reset(&obj, git2::Hard, sig, None));
+ try!(update_submodules(&repo, sig));
+ }
+ Ok(())
+ }
}
}
-
-fn git(path: &Path, cmd: ProcessBuilder) -> ProcessBuilder {
- debug!("Executing {} @ {}", cmd, path.display());
-
- cmd.cwd(path.clone())
-}
-
-fn git_inherit(path: &Path, cmd: ProcessBuilder) -> CargoResult<()> {
- let cmd = git(path, cmd);
- cmd.exec().chain_error(|| {
- human(format!("Executing {} failed", cmd))
- })
-}
-
-fn git_output(path: &Path, cmd: ProcessBuilder) -> CargoResult<String> {
- let cmd = git(path, cmd);
- let output = try!(cmd.exec_with_output().chain_error(||
- human(format!("Executing {} failed", cmd))));
-
- Ok(to_str(output.output.as_slice()).as_slice().trim_right().to_string())
-}
-
-fn to_str(vec: &[u8]) -> String {
- String::from_utf8_lossy(vec).into_string()
-}
-